[ISSUE #10987] Fix queryMinOffsetInAllGroup deleting consumer offsets from the live offset table - #10991
Conversation
…ffsets from the live offset table
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
This PR fixes a critical bug where queryMinOffsetInAllGroup was mutating the live offsetTable by deleting consumer offsets for filtered groups. The original code iterated over the live keySet and called removeConsumerOffset(), which destroyed offset data. The fix works on a snapshot of keys (new HashSet<>(this.offsetTable.keySet())) and uses removeIf on the snapshot instead. A comprehensive test verifies that filtered group offsets are preserved after the query.
LGTM — excellent fix for this data corruption bug!
Automated review by github-manager-bot
…sts so offset deletion and the malformed-key AIOOBE fail independently
Test evidence (before → after)The regression tests were verified in both directions on JDK 8 ( On the unfixed code (fix reverted, tests kept), the two regression tests now fail independently and each one demonstrates one defect:
Note the query returned while
With this PR: I split the originally single test into Side note on CI: the workflow runs for this PR are in |
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
This PR modifies 2 files (108 lines changed).
Automated scan completed. A maintainer should do a detailed review of the logic changes.
Files Changed
broker/src/main/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManager.javabroker/src/test/java/org/apache/rocketmq/broker/offset/ConsumerOffsetManagerTest.java
Automated review by github-manager-bot
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
This PR fixes a critical bug where queryMinOffsetInAllGroup was mutating the live offsetTable by removing filtered groups' offsets via Iterator.remove() and removeConsumerOffset(). The fix correctly works on a HashSet snapshot and uses the filtered set as a membership guard in the subsequent iteration — no more side-effects from a read-only query.
The added tests cover both the core regression (filtered group's offsets survive the query) and the edge case of malformed keys without the @ separator.
Looks good. 👍
Automated review by github-manager-bot
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #10991 +/- ##
=============================================
- Coverage 48.59% 48.55% -0.05%
+ Complexity 13680 13669 -11
=============================================
Files 1381 1381
Lines 101475 101473 -2
Branches 13190 13190
=============================================
- Hits 49313 49271 -42
- Misses 46163 46194 +31
- Partials 5999 6008 +9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
Fixes a critical data loss bug in queryMinOffsetInAllGroup — the query operation was mutating offsetTable by removing entries during iteration, destroying consumer offset data.
Observations
- ✅ Works on a snapshot of keys (
new HashSet<>(offsetTable.keySet())) instead of the live keySet, preventing ConcurrentModificationException and data mutation - ✅ Replaces unsafe iterator.remove() with
removeIfon the snapshot - ✅ Removes the destructive
removeConsumerOffset()call from the query path - ✅ Adds proper filtering logic in the main loop to skip filtered groups
- ✅ Includes two comprehensive tests:
testQueryMinOffsetInAllGroupDoesNotDeleteOffsets— verifies filtered groups' offsets are preservedtestQueryMinOffsetInAllGroupToleratesMalformedKeys— verifies graceful handling of malformed keys
Analysis
The original code had a severe bug: a read-only query operation was destroying data by calling removeConsumerOffset() during iteration. This would cause:
- Loss of consumer offset data for filtered groups
- Potential ConcurrentModificationException
- Incorrect min offset calculations
The fix correctly separates the filtering logic (on a snapshot) from the query logic (on the original data), ensuring the query is truly read-only.
LGTM — critical data loss bug fix.
Automated review by github-manager-bot
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
Critical data-loss bug: the read-only admin operation queryMinOffsetInAllGroup was mutating the live offsetTable by calling it.remove() on ConcurrentHashMap.keySet() (which is a live view). This silently deleted consumer offsets as a side effect of a query.
The fix correctly snapshots the key set into a HashSet before filtering, and adds a containsKey guard in the iteration loop. The removeIf with the arrays.length == 2 guard also makes the filter robust against malformed keys.
Two well-written tests: one verifies offsets survive the query, the other verifies malformed keys don't crash the operation.
LGTM.
Automated review by github-manager-bot
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
Fixes a critical data loss bug where queryMinOffsetInAllGroup() was mutating the live offsetTable through the keySet() iterator's remove(), which called removeConsumerOffset() and deleted consumer offsets as a side effect of a read-only query.
The fix:
- Works on a snapshot of keys (
new HashSet<>(keySet())) - Uses
removeIfon the copy, not the live table - Filters via the snapshot, then iterates the live table with a containment check
- Handles malformed keys (no
@separator) gracefully
Tests verify: filtered groups are excluded from min computation but their offsets are preserved; malformed keys do not break the query.
LGTM 👍
Automated review by github-manager-bot
RockteMQ-AI
left a comment
There was a problem hiding this comment.
Summary
Fixes a critical data loss bug where queryMinOffsetInAllGroup() was mutating the live offsetTable through the keySet() iterator's remove(), which called removeConsumerOffset() and deleted consumer offsets as a side effect of a read-only query.
The fix:
- Works on a snapshot of keys (
new HashSet<>(keySet())) - Uses
removeIfon the copy, not the live table - Filters via the snapshot, then iterates the live table with a containment check
- Handles malformed keys (no
@separator) gracefully
Tests verify: filtered groups are excluded from min computation but their offsets are preserved; malformed keys do not break the query.
LGTM 👍
Automated review by github-manager-bot
|
Evidence chain re-verified on 2026-09-05 (JDK 21, Maven 3.8.7), anchored to commits:
|
Which Issue(s) This PR Fixes
Brief Description
ConsumerOffsetManager#queryMinOffsetInAllGroup(topic, filterGroups)iterated the liveoffsetTable.keySet()and calledit.remove()on it to exclude the filter groups. SinceConcurrentHashMap.keySet()is a live view, running the read-only admin operationQUERY_CORRECTION_OFFSET(AdminBrokerProcessor#queryCorrectionOffset, exposed viaDefaultMQAdminExt#queryCorrectionOffset) permanently deleted everytopic@groupoffset entry of the filtered groups:persist()makes the deletion permanent (consumerOffset.json);RocksDBConsumerOffsetManager,removeConsumerOffsetdeletes the rows from RocksDB immediately;-1fromqueryOffsetand re-initialize perconsumeFromWhere→ mass duplicate consumption or skipping to max;topicAtGroup.split(TOPIC_GROUP_SEPARATOR)[1]also threwArrayIndexOutOfBoundsExceptionon a malformed key without@.This PR makes the exclusion work on a snapshot of the key set, so the query no longer mutates
offsetTableat all (and never callsremoveConsumerOffset), while preserving the original filter semantics: offsets of the filter groups are excluded from the min-offset computation. The malformed-key AIOOBE is fixed by checkingarrays.length == 2.Priority
PRIORITY = 76: impact 32 (a read-only admin query permanently deletes persisted consumer offsets — in memory,
consumerOffset.json, and RocksDB rows — so filtered groups re-initialize perconsumeFromWhere: mass duplicate consumption or skipping to max) + scope 12 (theQUERY_CORRECTION_OFFSETadmin API path of every broker) + reproducibility 18 (two deterministic regression tests, one per failure mode) + maintenance 14 (small snapshot-based fix that preserves the existing filter semantics). FIX_CONFIDENCE = 95.How Did You Test This Change?
Two regression tests in
ConsumerOffsetManagerTest(split so each failure mode is asserted independently):testQueryMinOffsetInAllGroupDoesNotDeleteOffsets: the filtered group is excluded from the min-offset computation, its offsets remain in the table (queryOffsetstill returns them) after the query, and the unfiltered query still returns the cross-group minimum.testQueryMinOffsetInAllGroupToleratesMalformedKeys: a stored key without@no longer breaks the query.Verified results (re-run 2026-09-05; after = branch tip 906bab3, before = base commit e348efa with the same test files):
mvn -pl broker test -Dtest=ConsumerOffsetManagerTest→ 7/7 pass.skipAfterFailureCount=1aborts a class run after the first error):testQueryMinOffsetInAllGroupDoesNotDeleteOffsets→ FAILURE:Expecting actual: {"Topic@G1"={0=50L}} to contain key: "Topic@G2"— the query deleted the filtered group's entry from the live offset table.testQueryMinOffsetInAllGroupToleratesMalformedKeys→ ERROR:java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1.Risk
Very low: the query now iterates a snapshot of the key set and never mutates
offsetTable(hence never callsremoveConsumerOffset); the filter semantics — excluding filter-group offsets from the min computation — are unchanged, and thearrays.length == 2guard only skips keys that previously threw. No public API or persistence format change.